// Copyright (c) .NET Foundation and contributors. All rights reserved.
// Licensed under the MIT license. See LICENSE file in the project root for full license information.

using System;
using System.Collections.Immutable;
using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using ILLink.RoslynAnalyzer.TrimAnalysis;
using ILLink.Shared.DataFlow;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.FlowAnalysis;
using Microsoft.CodeAnalysis.Operations;
namespace ILLink.RoslynAnalyzer.DataFlow
{
    // Visitor which tracks the values of locals in a block. It provides extension points that get called
    // whenever a value that comes from a tracked local reference flows into one of the following:
    // - field
    // - parameter
    // - method return
    public abstract class LocalDataFlowVisitor<TValue, TContext, TValueLattice, TContextLattice, TConditionValue> :
        OperationWalker<LocalDataFlowState<TValue, TContext, TValueLattice, TContextLattice>, TValue>,
        ITransfer<
            BlockProxy,
            LocalStateAndContext<TValue, TContext>,
            LocalDataFlowState<TValue, TContext, TValueLattice, TContextLattice>,
            LocalStateAndContextLattice<TValue, TContext, TValueLattice, TContextLattice>,
            TConditionValue>
        // This struct constraint prevents warnings due to possible null returns from the visitor methods.
        // Note that this assumes that default(TValue) is equal to the TopValue.
        where TValue : struct, IEquatable<TValue>
        where TContext : struct, IEquatable<TContext>
        where TValueLattice : ILattice<TValue>
        where TContextLattice : ILattice<TContext>
        where TConditionValue : struct, INegate<TConditionValue>
    {
        protected readonly LocalStateAndContextLattice<TValue, TContext, TValueLattice, TContextLattice> LocalStateAndContextLattice;

        protected readonly InterproceduralStateLattice<TValue, TValueLattice> InterproceduralStateLattice;

        protected readonly Compilation Compilation;

        protected readonly ISymbol OwningSymbol;

        private readonly ControlFlowGraph ControlFlowGraph;

        protected TValue TopValue => LocalStateAndContextLattice.LocalStateLattice.Lattice.ValueLattice.Top;

        private readonly ImmutableDictionary<CaptureId, FlowCaptureKind> lValueFlowCaptures;

        public InterproceduralState<TValue, TValueLattice> InterproceduralState;

        private bool IsLValueFlowCapture(CaptureId captureId)
            => lValueFlowCaptures.ContainsKey(captureId);

        private bool IsRValueFlowCapture(CaptureId captureId)
            => !lValueFlowCaptures.TryGetValue(captureId, out var captureKind) || captureKind != FlowCaptureKind.LValueCapture;

        public LocalDataFlowVisitor(
            Compilation compilation,
            LocalStateAndContextLattice<TValue, TContext, TValueLattice, TContextLattice> lattice,
            ISymbol owningSymbol,
            ControlFlowGraph cfg,
            ImmutableDictionary<CaptureId, FlowCaptureKind> lValueFlowCaptures,
            InterproceduralState<TValue, TValueLattice> interproceduralState)
        {
            Compilation = compilation;
            LocalStateAndContextLattice = lattice;
            InterproceduralStateLattice = default;
            OwningSymbol = owningSymbol;
            ControlFlowGraph = cfg;
            this.lValueFlowCaptures = lValueFlowCaptures;
            InterproceduralState = interproceduralState;
        }

        public abstract void ApplyCondition(TConditionValue condition, ref LocalStateAndContext<TValue, TContext> localContextState);

        public TConditionValue? Transfer(
            BlockProxy block,
            LocalDataFlowState<TValue, TContext, TValueLattice, TContextLattice> state)
        {
            foreach (IOperation operation in block.Block.Operations)
                Visit(operation, state);

            // Blocks may end with a BranchValue computation. Visit the BranchValue operation after all others.
            IOperation? branchValueOperation = block.Block.BranchValue;
            if (branchValueOperation == null)
                return null;

            var branchValue = Visit(branchValueOperation, state);
            TConditionValue conditionValue = GetConditionValue(branchValueOperation, state);
            if (block.Block.ConditionKind != ControlFlowConditionKind.None)
            {
                // BranchValue may represent a value used in a conditional branch to the ConditionalSuccessor.
                // If so, give the analysis an opportunity to model the checked condition, and return the model
                // of the condition back to the generic analysis. It will be applied to the state of each outgoing branch.
                return conditionValue;
            }

            // If not, the BranchValue represents a return or throw value associated with the FallThroughSuccessor of this block.
            // (ConditionalSuccessor == null iff ConditionKind == None).
            // If we get here, we should be analyzing code in a method or field/property initializer,
            // not an attribute instance, since attributes can't have throws or return statements
            Debug.Assert(OwningSymbol is IMethodSymbol or IFieldSymbol or IPropertySymbol,
                $"{OwningSymbol.GetType()}: {branchValueOperation.Syntax.GetLocation().GetLineSpan()}");

            // The BranchValue for a thrown value is not involved in dataflow tracking.
            if (block.Block.FallThroughSuccessor?.Semantics == ControlFlowBranchSemantics.Throw)
                return null;

            // Field/property initializers can't have return statements.
            Debug.Assert(OwningSymbol is IMethodSymbol,
                $"{OwningSymbol.GetType()}: {branchValueOperation.Syntax.GetLocation().GetLineSpan()}");

            // Return statements with return values are represented in the control flow graph as
            // a branch value operation that computes the return value.

            // Use the branch value operation as the key for the warning store and the location of the warning.
            // We don't want the return operation because this might have multiple possible return values in general.
            var current = state.Current;
            HandleReturnValue(branchValue, branchValueOperation, in current.Context);
            // Must be called for every return value even if it did not return an understood condition,
            // because the non-understood conditions will produce warnings for FeatureGuard properties.
            HandleReturnConditionValue(conditionValue, branchValueOperation);
            return null;
        }

        public abstract TConditionValue GetConditionValue(
            IOperation branchValueOperation,
            LocalDataFlowState<TValue, TContext, TValueLattice, TContextLattice> state);

        public abstract TValue GetFieldTargetValue(IFieldReferenceOperation fieldReference, in TContext context);

        public abstract TValue GetBackingFieldTargetValue(IPropertyReferenceOperation propertyReference, in TContext context);

        public abstract TValue GetParameterTargetValue(IParameterSymbol parameter);

        public abstract void HandleAssignment(
            TValue source,
            TValue target,
            IOperation operation,
            in TContext context);

        public abstract TValue HandleArrayElementRead(TValue arrayValue, TValue indexValue, IOperation operation);

        public abstract void HandleArrayElementWrite(TValue arrayValue, TValue indexValue, TValue valueToWrite, IOperation operation, bool merge);

        // This takes an IOperation rather than an IReturnOperation because the return value
        // may (must?) come from BranchValue of an operation whose FallThroughSuccessor is the exit block.
        public abstract void HandleReturnValue(
            TValue returnValue,
            IOperation operation,
            in TContext context);

        public abstract void HandleReturnConditionValue(
            TConditionValue returnConditionValue,
            IOperation branchValueOperation);

        // This is called for any method call, which includes:
        // - Normal invocation operation
        // - Accessing property value - which is treated as a call to the getter
        // - Setting a property value - which is treated as a call to the setter
        // All inputs are already visited and turned into values.
        // The return value should be a value representing the return value from the called method.
        public abstract TValue HandleMethodCall(
            IMethodSymbol calledMethod,
            TValue instance,
            ImmutableArray<TValue> arguments,
            IOperation operation,
            in TContext context);

        public override TValue VisitLocalReference(ILocalReferenceOperation operation, LocalDataFlowState<TValue, TContext, TValueLattice, TContextLattice> state)
        {
            return GetLocal(operation.Local, state);
        }

        private TValue ProcessBinderCall(IOperation operation, string methodName, LocalDataFlowState<TValue, TContext, TValueLattice, TContextLattice> state)
        {
            var assemblyType = Compilation.GetTypeByMetadataName("Microsoft.CSharp.RuntimeBinder.Binder");
            Debug.Assert(assemblyType != null);
            if (assemblyType == null)
                return TopValue;
            var method = assemblyType.GetMembers(methodName).OfType<IMethodSymbol>().SingleOrDefault();
            Debug.Assert(method != null);
            if (method == null)
                return TopValue;
            return ProcessMethodCall(operation, method, null, ImmutableArray<IArgumentOperation>.Empty, state);
        }

        public override TValue VisitDynamicInvocation(IDynamicInvocationOperation operation, LocalDataFlowState<TValue, TContext, TValueLattice, TContextLattice> state)
            => ProcessBinderCall(operation, "InvokeMember", state);

        public override TValue VisitDynamicObjectCreation(IDynamicObjectCreationOperation operation, LocalDataFlowState<TValue, TContext, TValueLattice, TContextLattice> state)
            => ProcessBinderCall(operation, "InvokeConstructor", state);

        public override TValue VisitDynamicMemberReference(IDynamicMemberReferenceOperation operation, LocalDataFlowState<TValue, TContext, TValueLattice, TContextLattice> state)
            => ProcessBinderCall(operation, operation.GetValueUsageInfo(OwningSymbol).HasFlag(ValueUsageInfo.Write) ? "SetMember" : "GetMember", state);

        public override TValue VisitDynamicIndexerAccess(IDynamicIndexerAccessOperation operation, LocalDataFlowState<TValue, TContext, TValueLattice, TContextLattice> state)
            => ProcessBinderCall(operation, operation.GetValueUsageInfo(OwningSymbol).HasFlag(ValueUsageInfo.Write) ? "SetIndex" : "GetIndex", state);

        private bool IsCapturedVariable(ILocalSymbol local)
        {
            if (local.IsConst)
                return false;
            Debug.Assert(local.ContainingSymbol is IMethodSymbol or IFieldSymbol, // backing field for property initializers
                $"{local.ContainingSymbol.GetType()}: {local.Locations[0].GetLineSpan()}");
            return !ReferenceEquals(local.ContainingSymbol, OwningSymbol);
        }

        private TValue GetLocal(ILocalSymbol symbol, LocalDataFlowState<TValue, TContext, TValueLattice, TContextLattice> state)
        {
            var local = new LocalKey(symbol);
            if (IsCapturedVariable(symbol))
                InterproceduralState.TrackHoistedLocal(local);

            // Get the value from the hoisted locals, if it's tracked there.
            if (InterproceduralState.TryGetHoistedLocal(local, out TValue? value))
                return value.Value;

            return state.Get(local);
        }

        private void SetLocal(ILocalSymbol localSymbol, TValue value, LocalDataFlowState<TValue, TContext, TValueLattice, TContextLattice> state, bool merge = false)
        {
            var local = new LocalKey(localSymbol);
            if (IsCapturedVariable(localSymbol))
                InterproceduralState.TrackHoistedLocal(local);

            // Update the value stored in the hoisted locals, if it's tracked there.
            if (InterproceduralState.TrySetHoistedLocal(local, value))
                return;

            var newValue = merge
                ? state.Lattice.LocalStateLattice.Lattice.ValueLattice.Meet(state.Get(local), value)
                : value;
            state.Set(local, newValue);
        }

        private TValue ProcessSingleTargetAssignment(
            IOperation targetOperation,
            IAssignmentOperation operation,
            LocalDataFlowState<TValue, TContext, TValueLattice, TContextLattice> state,
            bool merge
        )
        {
            return ProcessSingleTargetAssignment(targetOperation, operation.Value, operation, state, merge);
        }

        private TValue ProcessSingleTargetAssignment(
            IOperation targetOperation,
            IOperation valueOperation,
            IOperation assignmentOperation,
            LocalDataFlowState<TValue, TContext, TValueLattice, TContextLattice> state,
            bool merge
        )
        {
            switch (targetOperation)
            {
                case IFieldReferenceOperation:
                case IParameterReferenceOperation:
                {
                    var current = state.Current;
                    TValue targetValue = targetOperation switch
                    {
                        IFieldReferenceOperation fieldRef => GetFieldTargetValue(fieldRef, in current.Context),
                        IParameterReferenceOperation parameterRef => GetParameterTargetValue(parameterRef.Parameter),
                        _ => throw new InvalidOperationException()
                    };
                    TValue value = Visit(valueOperation, state);
                    HandleAssignment(value, targetValue, assignmentOperation, in current.Context);
                    return value;
                }

                // The remaining cases don't have a dataflow value that represents LValues, so we need
                // to handle the LHS specially.
                case IPropertyReferenceOperation propertyRef:
                {
                    // Avoid visiting the property reference because for captured properties, we can't
                    // correctly detect whether it is used for reading or writing inside of VisitPropertyReference.
                    // https://github.com/dotnet/roslyn/issues/25057
                    TValue instanceValue = Visit(propertyRef.Instance, state);
                    TValue value = Visit(valueOperation, state);
                    IMethodSymbol? setMethod = propertyRef.Property.GetSetMethod();

                    if (setMethod == null ||
                        (OwningSymbol is IPropertySymbol && (ControlFlowGraph.OriginalOperation is not IAttributeOperation)))
                    {
                        // This can be a write to a byref return of a property getter.
                        // Skip for now: https://github.com/dotnet/linker/issues/2158
                        if (propertyRef.Property.RefKind is not RefKind.None)
                            break;

                        // This can happen in a constructor - there it is possible to assign to a property
                        // without a setter. This turns into an assignment to the compiler-generated backing field.
                        // Handle this similarly to field assignments.

                        // Even if the property has a set method, if the assignment takes place in a property initializer,
                        // the write becomes a direct write to the underlying field. This should be treated the same as
                        // the case where there is no set method.

                        var current = state.Current;
                        TValue targetValue = GetBackingFieldTargetValue(propertyRef, in current.Context);
                        HandleAssignment(value, targetValue, assignmentOperation, in current.Context);
                        return value;
                    }

                    ImmutableArray<TValue>.Builder arguments = ImmutableArray.CreateBuilder<TValue>();

                    // Handle C# 14 extension property access (see comment in ProcessMethodCall)
                    if (setMethod.HasExtensionParameterOnType())
                    {
                        arguments.Add(instanceValue);
                        instanceValue = TopValue;
                    }

                    // Property may be an indexer, in which case there will be one or more index arguments followed by a value argument
                    foreach (var val in propertyRef.Arguments)
                        arguments.Add(Visit(val, state));
                    arguments.Add(value);

                    HandleMethodCallHelper(setMethod, instanceValue, arguments.ToImmutableArray(), assignmentOperation, state);
                    // The return value of a property set expression is the value,
                    // even though a property setter has no return value.
                    return value;
                }
                case IEventReferenceOperation eventRef:
                {
                    // Handles assignment to an event like 'Event = Handler;', which is a write to the underlying field,
                    // not a call to an event accessor method. There is no Roslyn API to access the field,
                    // so just visit the instance and the value. https://github.com/dotnet/roslyn/issues/40103
                    Visit(eventRef.Instance, state);
                    return Visit(valueOperation, state);
                }
                case IImplicitIndexerReferenceOperation indexerRef:
                {
                    // An implicit reference to an indexer where the argument is a System.Index
                    TValue instanceValue = Visit(indexerRef.Instance, state);
                    TValue indexArgumentValue = Visit(indexerRef.Argument, state);
                    TValue value = Visit(valueOperation, state);

                    var property = (IPropertySymbol)indexerRef.IndexerSymbol;

                    var argumentsBuilder = ImmutableArray.CreateBuilder<TValue>();
                    argumentsBuilder.Add(indexArgumentValue);
                    argumentsBuilder.Add(value);

                    IMethodSymbol? setMethod = property.GetSetMethod();
                    if (setMethod == null)
                    {
                        // It might actually be a call to a ref-returning get method,
                        // like Span<T>.this[int].get. We don't handle ref returns yet.
                        break;
                    }

                    HandleMethodCallHelper(setMethod, instanceValue, argumentsBuilder.ToImmutableArray(), assignmentOperation, state);
                    return value;
                }

                // TODO: when setting a property in an attribute, target is an IPropertyReference.
                case ILocalReferenceOperation localRef:
                {
                    TValue value = Visit(valueOperation, state);
                    SetLocal(localRef.Local, value, state, merge);
                    return value;
                }
                case IDeclarationPatternOperation declPattern:
                {
                    if (declPattern.DeclaredSymbol is not ILocalSymbol declaredSymbol)
                        break;
                    var value = Visit(valueOperation, state);
                    SetLocal(declaredSymbol, value, state, merge);
                    return value;
                }
                case IArrayElementReferenceOperation arrayElementRef:
                {
                    if (arrayElementRef.Indices.Length != 1)
                        break;

                    TValue arrayRef = Visit(arrayElementRef.ArrayReference, state);
                    TValue index = Visit(arrayElementRef.Indices[0], state);
                    TValue value = Visit(valueOperation, state);
                    HandleArrayElementWrite(arrayRef, index, value, assignmentOperation, merge: merge);
                    return value;
                }
                case IInlineArrayAccessOperation inlineArrayAccess:
                {
                    TValue arrayRef = Visit(inlineArrayAccess.Instance, state);
                    TValue index = Visit(inlineArrayAccess.Argument, state);
                    TValue value = Visit(valueOperation, state);
                    HandleArrayElementWrite(arrayRef, index, value, assignmentOperation, merge: merge);
                    return value;
                }
                case IDiscardOperation:
                    // Assignments like "_ = SomeMethod();" don't need dataflow tracking.
                    // Seems like this can't happen with a flow capture operation.
                    Debug.Assert(targetOperation is not IFlowCaptureReferenceOperation);
                    break;
                case IInstanceReferenceOperation:
                // Assignment to 'this' is not tracked currently.
                // Not relevant for trimming dataflow.
                case IInvocationOperation:
                // This can happen for an assignment to a ref return. Skip for now.
                // The analyzer doesn't handle refs yet. This should be fixed once the analyzer
                // also produces warnings for ref params/locals/returns.
                // https://github.com/dotnet/linker/issues/2632
                // https://github.com/dotnet/linker/issues/2158
                case IDynamicMemberReferenceOperation:
                case IDynamicIndexerAccessOperation:
                    // Assignment to dynamic member/indexer will translate into a call to runtime binder methods
                    // which should produce warnings, but isn't relevant for dataflow.
                    Visit(targetOperation, state);
                    break;

                // Keep these cases in sync with those in CapturedReferenceValue, for any that
                // can show up in a flow capture reference (for example, where the right-hand side
                // is a null-coalescing operator).
                default:
                    UnexpectedOperationHandler.Handle(targetOperation);
                    break;
            }
            return Visit(valueOperation, state);
        }

        public override TValue VisitSimpleAssignment(ISimpleAssignmentOperation operation, LocalDataFlowState<TValue, TContext, TValueLattice, TContextLattice> state)
        {
            return ProcessAssignment(operation, state);
        }

        public override TValue VisitIsPattern(IIsPatternOperation operation, LocalDataFlowState<TValue, TContext, TValueLattice, TContextLattice> state)
        {
            if (operation.Pattern is IDeclarationPatternOperation declarationPattern)
            {
                // A declaration pattern is like an assignment to a local
                return ProcessSingleTargetAssignment(
                    declarationPattern,
                    operation.Value,
                    operation,
                    state,
                    merge: false
                );
            }
            return base.VisitIsPattern(operation, state);
        }

        public override TValue VisitPropertySubpattern(IPropertySubpatternOperation propPattern, LocalDataFlowState<TValue, TContext, TValueLattice, TContextLattice> state)
        {
            if (propPattern.Pattern is IDeclarationPatternOperation declPattern)
            {
                return ProcessSingleTargetAssignment(
                    declPattern,
                    propPattern.Member,
                    propPattern,
                    state,
                    merge: false
                );
            }
            return base.VisitPropertySubpattern(propPattern, state);
        }

        public override TValue VisitCompoundAssignment(ICompoundAssignmentOperation operation, LocalDataFlowState<TValue, TContext, TValueLattice, TContextLattice> state)
        {
            return ProcessAssignment(operation, state);
        }

        // Note: this is called both for normal assignments and ICompoundAssignmentOperation.
        // The resulting value of a compound assignment isn't important for our dataflow analysis
        // (we don't model addition of integers, for example), so we just treat these the same
        // as normal assignments.
        private TValue ProcessAssignment(IAssignmentOperation operation, LocalDataFlowState<TValue, TContext, TValueLattice, TContextLattice> state)
        {
            var targetOperation = operation.Target;
            if (targetOperation is not IFlowCaptureReferenceOperation flowCaptureReference)
                return ProcessSingleTargetAssignment(targetOperation, operation, state, merge: false);

            // Note: technically we should avoid visiting the target operation in ProcessNonCapturedAssignment when assigning
            // to a flow capture reference, because this should be done when the capture is created.
            // For example, a flow capture used as both an LValue and a RValue should only evaluate the expression that
            // computes the object instance of a property reference once. However, we just visit the instance again below
            // for simplicity. This could be generalized if we encounter a dataflow behavior where this makes a difference.

            Debug.Assert(IsLValueFlowCapture(flowCaptureReference.Id));
            Debug.Assert(flowCaptureReference.GetValueUsageInfo(OwningSymbol).HasFlag(ValueUsageInfo.Write));
            var capturedReferences = state.Current.LocalState.CapturedReferences.Get(flowCaptureReference.Id);
            Debug.Assert(!capturedReferences.IsUnknown());
            if (!capturedReferences.HasMultipleValues)
            {
                // Single captured reference. Treat this as an overwriting assignment.
                var enumerator = capturedReferences.GetKnownValues().GetEnumerator();
                enumerator.MoveNext();
                targetOperation = enumerator.Current.Reference;
                return ProcessSingleTargetAssignment(targetOperation, operation, state, merge: false);
            }

            // The capture id may have captured multiple references, as in:
            // (b ? ref v1 : ref v2) = value;
            // We treat this as a possible write to each of the captured references,
            // which requires merging with the previous values of each.

            // Note: technically this should only visit the RHS of the assignment once.
            // For now we visit the RHS in ProcessSingleTargetAssignment for simplicity, and
            // rely on the warning deduplication to prevent this from producing multiple warnings
            // if the RHS has dataflow warnings.

            TValue value = TopValue;
            foreach (var capturedReference in capturedReferences.GetKnownValues())
            {
                targetOperation = capturedReference.Reference;
                var singleValue = ProcessSingleTargetAssignment(targetOperation, operation, state, merge: true);
                value = LocalStateAndContextLattice.LocalStateLattice.Lattice.ValueLattice.Meet(value, singleValue);
            }

            return value;
        }

        public override TValue VisitEventAssignment(IEventAssignmentOperation operation, LocalDataFlowState<TValue, TContext, TValueLattice, TContextLattice> state)
        {
            var eventReference = (IEventReferenceOperation)operation.EventReference;
            TValue instanceValue = Visit(eventReference.Instance, state);
            TValue value = Visit(operation.HandlerValue, state);
            if (operation.Adds)
            {
                IMethodSymbol? addMethod = eventReference.Event.AddMethod;
                Debug.Assert(addMethod != null);
                if (addMethod != null)
                    HandleMethodCallHelper(addMethod, instanceValue, ImmutableArray.Create(value), operation, state);
                return value;
            }
            else
            {
                IMethodSymbol? removeMethod = eventReference.Event.RemoveMethod;
                Debug.Assert(removeMethod != null);
                if (removeMethod != null)
                    HandleMethodCallHelper(removeMethod, instanceValue, ImmutableArray.Create(value), operation, state);
                return value;
            }
        }

        private TValue GetFlowCaptureValue(IFlowCaptureReferenceOperation operation, LocalDataFlowState<TValue, TContext, TValueLattice, TContextLattice> state)
        {
            Debug.Assert(!IsLValueFlowCapture(operation.Id),
                $"{operation.Syntax.GetLocation().GetLineSpan()}");
            Debug.Assert(operation.GetValueUsageInfo(OwningSymbol).HasFlag(ValueUsageInfo.Read),
                $"{operation.Syntax.GetLocation().GetLineSpan()}");

            return state.Get(new LocalKey(operation.Id));
        }

        // Similar to VisitLocalReference
        public override TValue VisitFlowCaptureReference(IFlowCaptureReferenceOperation operation, LocalDataFlowState<TValue, TContext, TValueLattice, TContextLattice> state)
        {
            if (operation.IsInitialization)
            {
                // This capture reference is a temporary byref. This can happen for string
                // interpolation handlers: https://github.com/dotnet/roslyn/issues/57484.
                // Should really be treated as creating a new l-value flow capture,
                // but this is likely irrelevant for dataflow analysis.

                // LValueFlowCaptureProvider doesn't take into account IsInitialization = true,
                // so it doesn't properly detect this as an l-value capture.
                // Context: https://github.com/dotnet/roslyn/issues/60757
                // Debug.Assert(IsLValueFlowCapture(operation.Id));
                Debug.Assert(operation.GetValueUsageInfo(OwningSymbol).HasFlag(ValueUsageInfo.Write),
                    $"{operation.Syntax.GetLocation().GetLineSpan()}");
                Debug.Assert(operation.GetValueUsageInfo(OwningSymbol).HasFlag(ValueUsageInfo.Reference),
                    $"{operation.Syntax.GetLocation().GetLineSpan()}");
                return TopValue;
            }

            if (operation.GetValueUsageInfo(OwningSymbol).HasFlag(ValueUsageInfo.Write))
            {
                // If we get here, it means we're visiting a flow capture reference that may be
                // assigned to. Similar to the IsInitialization case, this can happen for an out param
                // where the variable is declared before being passed as an out param, for example:

                // string s;
                // Method(out s, b ? 0 : 1);

                // The second argument is necessary to create multiple branches so that the compiler
                // turns both arguments into flow capture references, instead of just passing a local
                // reference for s.

                // This can also happen for a deconstruction assignments, where the write is not to a byref.
                // Once the analyzer implements support for deconstruction assignments (https://github.com/dotnet/linker/issues/3158),
                // we can try enabling this assert to ensure that this case is only hit for byrefs.
                // Debug.Assert(operation.GetValueUsageInfo(OwningSymbol).HasFlag(ValueUsageInfo.Reference),
                //     $"{operation.Syntax.GetLocation().GetLineSpan()}");
                return TopValue;
            }

            return GetFlowCaptureValue(operation, state);
        }

        // Similar to VisitSimpleAssignment when assigning to a local, but for values which are captured without a
        // corresponding local variable. The "flow capture" is like a local assignment, and the "flow capture reference"
        // is like a local reference.
        public override TValue VisitFlowCapture(IFlowCaptureOperation operation, LocalDataFlowState<TValue, TContext, TValueLattice, TContextLattice> state)
        {
            if (IsLValueFlowCapture(operation.Id))
            {
                // Should never see an l-value flow capture of another flow capture.
                Debug.Assert(operation.Value is not IFlowCaptureReferenceOperation);
                if (operation.Value is IFlowCaptureReferenceOperation)
                    return TopValue;

                // Note: technically we should save some information about the value for LValue flow captures
                // (for example, the object instance of a property reference) and avoid re-computing it when
                // assigning to the FlowCaptureReference.
                var capturedRef = new CapturedReferenceValue(operation.Value);
                var currentState = state.Current;
                currentState.LocalState.CapturedReferences.Set(operation.Id, capturedRef);
                state.Current = currentState;
                return TopValue;
            }
            else
            {
                TValue capturedValue;
                if (operation.Value is IFlowCaptureReferenceOperation captureRef)
                {
                    if (IsLValueFlowCapture(captureRef.Id))
                    {
                        // If an r-value captures an l-value, we must dereference the l-value
                        // and copy out the value to capture.
                        capturedValue = TopValue;
                        var capturedReferences = state.Current.LocalState.CapturedReferences.Get(captureRef.Id);
                        Debug.Assert(!capturedReferences.IsUnknown());
                        foreach (var capturedReference in capturedReferences.GetKnownValues())
                        {
                            var value = Visit(capturedReference.Reference, state);
                            capturedValue = LocalStateAndContextLattice.LocalStateLattice.Lattice.ValueLattice.Meet(capturedValue, value);
                        }
                    }
                    else
                    {
                        capturedValue = state.Get(new LocalKey(captureRef.Id));
                    }
                }
                else
                {
                    capturedValue = Visit(operation.Value, state);
                }

                state.Set(new LocalKey(operation.Id), capturedValue);
                return capturedValue;
            }
        }

        public override TValue VisitExpressionStatement(IExpressionStatementOperation operation, LocalDataFlowState<TValue, TContext, TValueLattice, TContextLattice> state)
        {
            Visit(operation.Operation, state);
            return TopValue;
        }

        public override TValue VisitInvocation(IInvocationOperation operation, LocalDataFlowState<TValue, TContext, TValueLattice, TContextLattice> state)
            => ProcessMethodCall(operation, operation.TargetMethod, operation.Instance, operation.Arguments, state);

        public override TValue VisitDelegateCreation(IDelegateCreationOperation operation, LocalDataFlowState<TValue, TContext, TValueLattice, TContextLattice> state)
        {
            Visit(operation.Target, state);

            IMethodSymbol? targetMethodSymbol = null;
            switch (operation.Target)
            {
                case IFlowAnonymousFunctionOperation lambda:
                    // Tracking lambdas is handled by normal visiting logic for IFlowAnonymousFunctionOperation.

                    // Instance of a lambda or local function should be the instance of the containing method.
                    // Don't need to track a dataflow value, since the delegate creation will warn if the
                    // lambda or local function has an annotated this parameter.
                    targetMethodSymbol = lambda.Symbol;
                    break;
                case IMethodReferenceOperation methodReference:
                    IMethodSymbol methodDefinition = methodReference.Method.OriginalDefinition;
                    if (methodDefinition.ContainingSymbol is IMethodSymbol)
                    {
                        // Track references to local functions
                        var localFunction = methodDefinition;
                        Debug.Assert(localFunction.MethodKind == MethodKind.LocalFunction);
                        var localFunctionCFG = ControlFlowGraph.GetLocalFunctionControlFlowGraphInScope(localFunction);
                        InterproceduralState.TrackMethod(new MethodBodyValue(localFunction, localFunctionCFG));
                    }
                    targetMethodSymbol = methodReference.Method;
                    break;
                case IMemberReferenceOperation:
                case IInvocationOperation:
                    // No method symbol.
                    break;
                default:
                    UnexpectedOperationHandler.Handle(operation.Target);
                    break;
            }

            if (targetMethodSymbol == null)
                return TopValue;

            return HandleDelegateCreation(targetMethodSymbol, operation, state.Current.Context);
        }

        public abstract TValue HandleDelegateCreation(IMethodSymbol methodReference, IOperation operation, in TContext context);

        public override TValue VisitPropertyReference(IPropertyReferenceOperation operation, LocalDataFlowState<TValue, TContext, TValueLattice, TContextLattice> state)
        {
            if (operation.GetValueUsageInfo(OwningSymbol).HasFlag(ValueUsageInfo.Write))
            {
                // Property references may be passed as ref/out parameters.
                // Enable this assert once we have support for deconstruction assignments.
                // https://github.com/dotnet/linker/issues/3158
                // Debug.Assert(operation.GetValueUsageInfo(OwningSymbol).HasFlag(ValueUsageInfo.Reference),
                //   $"{operation.Syntax.GetLocation().GetLineSpan()}");
                return TopValue;
            }

            // Accessing property for reading is really a call to the getter
            // The setter case is handled in assignment operation since here we don't have access to the value to pass to the setter
            TValue instanceValue = Visit(operation.Instance, state);
            IMethodSymbol getMethod = operation.Property.GetGetMethod()!;
            ImmutableArray<TValue>.Builder arguments = ImmutableArray.CreateBuilder<TValue>();
            // Handle C# 14 extension property access (see comment in ProcessMethodCall)
            if (getMethod.HasExtensionParameterOnType())
            {
                arguments.Add(instanceValue);
                instanceValue = TopValue;
            }

            // Property may be an indexer, in which case there will be one or more index arguments
            foreach (var val in operation.Arguments)
                arguments.Add(Visit(val, state));

            return HandleMethodCallHelper(getMethod, instanceValue, arguments.ToImmutableArray(), operation, state);
        }

        public override TValue VisitEventReference(IEventReferenceOperation operation, LocalDataFlowState<TValue, TContext, TValueLattice, TContextLattice> state)
        {
            // Writing to an event should not go through this path.
            Debug.Assert(operation.GetValueUsageInfo(OwningSymbol).HasFlag(ValueUsageInfo.Read));
            if (!operation.GetValueUsageInfo(OwningSymbol).HasFlag(ValueUsageInfo.Read))
                return TopValue;

            Visit(operation.Instance, state);
            // Accessing event for reading retrieves the event delegate from the event's backing field,
            // so there is no method call to handle.
            return TopValue;
        }

        public override TValue VisitImplicitIndexerReference(IImplicitIndexerReferenceOperation operation, LocalDataFlowState<TValue, TContext, TValueLattice, TContextLattice> state)
        {
            if (operation.GetValueUsageInfo(OwningSymbol).HasFlag(ValueUsageInfo.Write))
            {
                // Implicit indexer references may be passed as ref/out parameters.
                Debug.Assert(operation.GetValueUsageInfo(OwningSymbol).HasFlag(ValueUsageInfo.Reference));
                return TopValue;
            }

            TValue instanceValue = Visit(operation.Instance, state);
            TValue indexArgumentValue = Visit(operation.Argument, state);

            if (operation.IndexerSymbol is not IPropertySymbol indexerProperty)
            {
                // For example, System.Span<T>.Slice(int, int).
                // Don't try to handle it for now.
                return TopValue;
            }

            IMethodSymbol getMethod = indexerProperty.GetGetMethod()!;
            return HandleMethodCallHelper(getMethod, instanceValue, ImmutableArray.Create(indexArgumentValue), operation, state);
        }

        public override TValue VisitArrayElementReference(IArrayElementReferenceOperation operation, LocalDataFlowState<TValue, TContext, TValueLattice, TContextLattice> state)
        {
            if (!operation.GetValueUsageInfo(OwningSymbol).HasFlag(ValueUsageInfo.Read))
                return TopValue;

            // Accessing an array element for reading is a call to the indexer
            // or a plain array access. Just handle plain array access for now.

            // Only handle simple index access
            if (operation.Indices.Length != 1)
                return TopValue;

            return HandleArrayElementRead(Visit(operation.ArrayReference, state), Visit(operation.Indices[0], state), operation);
        }

        public override TValue VisitInlineArrayAccess(IInlineArrayAccessOperation operation, LocalDataFlowState<TValue, TContext, TValueLattice, TContextLattice> state)
        {
            Debug.Assert(operation.GetValueUsageInfo(OwningSymbol).HasFlag(ValueUsageInfo.Read));
            if (!operation.GetValueUsageInfo(OwningSymbol).HasFlag(ValueUsageInfo.Read))
                return TopValue;

            return HandleArrayElementRead(Visit(operation.Instance, state), Visit(operation.Argument, state), operation);
        }

        public override TValue VisitArgument(IArgumentOperation operation, LocalDataFlowState<TValue, TContext, TValueLattice, TContextLattice> state)
        {
            return Visit(operation.Value, state);
        }

        public override TValue VisitReturn(IReturnOperation operation, LocalDataFlowState<TValue, TContext, TValueLattice, TContextLattice> state)
        {
            if (operation.ReturnedValue != null)
            {
                var value = Visit(operation.ReturnedValue, state);
                var current = state.Current;
                HandleReturnValue(value, operation, in current.Context);
                return value;
            }

            return TopValue;
        }

        public override TValue VisitConversion(IConversionOperation operation, LocalDataFlowState<TValue, TContext, TValueLattice, TContextLattice> state)
        {
            var operandValue = Visit(operation.Operand, state);
            return operation.OperatorMethod == null ? operandValue : TopValue;
        }

        public override TValue VisitObjectCreation(IObjectCreationOperation operation, LocalDataFlowState<TValue, TContext, TValueLattice, TContextLattice> state)
        {
            if (operation.Constructor == null)
                return TopValue;

            return ProcessMethodCall(operation, operation.Constructor, null, operation.Arguments, state);
        }

        public override TValue VisitFlowAnonymousFunction(IFlowAnonymousFunctionOperation operation, LocalDataFlowState<TValue, TContext, TValueLattice, TContextLattice> state)
        {
            // The containing symbol of a lambda is either another method, or a field (for field initializers).
            // For property initializers, the containing symbol is the compiler-generated backing field.
            // For property accessors, the containing symbol is the accessor method.
            Debug.Assert(operation.Symbol.ContainingSymbol is IMethodSymbol or IFieldSymbol);
            var lambda = operation.Symbol;
            Debug.Assert(lambda.MethodKind == MethodKind.LambdaMethod);
            var lambdaCFG = ControlFlowGraph.GetAnonymousFunctionControlFlowGraphInScope(operation);
            InterproceduralState.TrackMethod(new MethodBodyValue(lambda, lambdaCFG));
            return TopValue;
        }

        private TValue HandleMethodCallHelper(
            IMethodSymbol calledMethod,
            TValue instance,
            ImmutableArray<TValue> arguments,
            IOperation operation,
            LocalDataFlowState<TValue, TContext, TValueLattice, TContextLattice> state)
        {
            var value = HandleMethodCall(calledMethod, instance, arguments, operation, state.Current.Context);

            if (calledMethod.TryGetAttribute(nameof(DoesNotReturnAttribute), out var doesNotReturnAttributeData))
            {
                // If it doesn't return, then after the called method we are free to assume that we
                // are in the Top state. This will have the correct behavior if meeting with another
                // state in the CFG. For example:

                // var x = SomeTrackedValue;
                // if (SomeCondition)
                //   DoesNotReturn();
                // WitnessState(x);

                // The state at 'WitnessState(x)' and 'WitnessContext()' is computed as the meet of:
                // - the output at the end of the block inside of the if (Top), and
                // - the fall-through output of the block with the check (x: SomeTrackedValue).
                // Because Top is the identity of meet, the state from before the if block is preserved,
                // so 'WitnessState(x)' sees that x has 'SomeTrackedValue' in this case.
                state.Current = LocalStateAndContextLattice.Top;
                return value;
            }

            foreach (var parameterProxy in calledMethod.GetParameters())
            {
                if (parameterProxy.ParameterSymbol is not IParameterSymbol parameter)
                    continue;

                if (!parameter.TryGetAttribute(nameof(DoesNotReturnIfAttribute), out var attributeData))
                    continue;

                if (attributeData.ConstructorArguments.Length != 1)
                    continue;

                var attributeArgument = attributeData.ConstructorArguments[0];
                if (attributeArgument.Kind != TypedConstantKind.Primitive)
                    continue;

                if (attributeArgument.Value is not bool doesNotReturnIfConditionValue)
                    continue;

                var argumentIndex = parameterProxy.MetadataIndex;
                var argument = arguments[argumentIndex];

                IArgumentOperation argumentOperation;
                switch (operation)
                {
                    case IInvocationOperation callOperation:
                        argumentOperation = callOperation.Arguments[argumentIndex];
                        break;
                    case IObjectCreationOperation callOperation:
                        argumentOperation = callOperation.Arguments[argumentIndex];
                        break;
                    default:
                        UnexpectedOperationHandler.Handle(operation);
                        continue;
                }
                ;

                // Get the condition value that is being asserted. If the attribute is DoesNotReturnIf(true),
                // the condition value needs to be negated so that we can assert the false condition.
                TConditionValue conditionValue = GetConditionValue(argumentOperation, state);
                var current = state.Current;
                ApplyCondition(
                    !doesNotReturnIfConditionValue
                        ? conditionValue
                        : conditionValue.Negate(),
                    ref current);
                state.Current = current;
            }

            return value;
        }

        private TValue ProcessMethodCall(
            IOperation operation,
            IMethodSymbol method,
            IOperation? instance,
            ImmutableArray<IArgumentOperation> arguments,
            LocalDataFlowState<TValue, TContext, TValueLattice, TContextLattice> state)
        {
            TValue instanceValue = Visit(instance, state);

            var argumentsBuilder = ImmutableArray.CreateBuilder<TValue>();

            // For calls to C# 14 extensions, treat the instance argument as a regular argument.
            // The extension method doesn't have an implicit this, yet (unlike for existing extension methods)
            // the IOperation represents it as having an Instance.
            if (method.HasExtensionParameterOnType())
            {
                argumentsBuilder.Add(instanceValue);
                instanceValue = TopValue;
            }

            // Build a map from parameter to argument so we can handle named/out-of-order arguments correctly
            var parameters = method.Parameters;
            var argumentsByParameter = new TValue?[parameters.Length];

            foreach (var argument in arguments)
            {
                // For __arglist argument there might not be any parameter
                // __arglist is only legal as the last argument to a method and there's also no supported
                // way for it to carry annotations or participate in data flow in any way, so it's OK to ignore it.
                // Since it's always last, it's also OK to simply pass a shorter arguments array to the call.
                if (argument?.Parameter == null)
                    break;

                var parameter = argument.Parameter;
                // Use Ordinal to get the parameter's position
                int parameterIndex = parameter.Ordinal;
                if (parameterIndex >= 0 && parameterIndex < parameters.Length)
                {
                    argumentsByParameter[parameterIndex] = VisitArgument(argument, state);
                }
            }

            // Add arguments in parameter order, using TopValue for missing arguments (e.g., optional parameters)
            for (int i = 0; i < parameters.Length; i++)
            {
                argumentsBuilder.Add(argumentsByParameter[i] ?? TopValue);
            }

            // For local functions with generic arguments, the substituted method symbol's containing
            // symbol is not the containing method, so we need to check the OriginalDefinition.
            if (method.OriginalDefinition.ContainingSymbol is IMethodSymbol)
            {
                var localFunction = method.OriginalDefinition;
                Debug.Assert(localFunction.MethodKind == MethodKind.LocalFunction);
                var localFunctionCFG = ControlFlowGraph.GetLocalFunctionControlFlowGraphInScope(localFunction);
                InterproceduralState.TrackMethod(new MethodBodyValue(localFunction, localFunctionCFG));
            }

            return HandleMethodCallHelper(
                method,
                instanceValue,
                argumentsBuilder.ToImmutableArray(),
                operation,
                state);
        }
    }
}
